﻿using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;

namespace SdmxMl.Data
{
    /// <summary>Class for creating chunks of huge CSV files </summary>
    public class CsvChuncher
    {
        #region Internal members

        private const string infolistExt = ".aListCk";
        private string csvPath;
        private string chunkListPath;
        private CsvProperties properties;
        private string header;

        #endregion

        #region Properties

        public CsvChunkList ChunkList { get; private set; }

        #endregion

        #region C'tor

        /// <summary> Empty C'tor </summary>
        public CsvChuncher()
        {
            properties = new CsvProperties();
            properties.Delimiter = ';';
        }

        /// <summary> Parametrized C'tor </summary>
        public CsvChuncher(CsvProperties p)
        {
            properties = p;
        }
        #endregion

        #region Chunks creation

        /// <summary> Create a chunked representation of CSV file</summary>
        /// <param name="csvPath">The CSV file path</param>
        /// <returns>THe path of chunked list info file</returns>
        public string CreateChunks(string csvPath)
        {
            ChunkList = new CsvChunkList();
            this.csvPath = csvPath;

            string fname = Path.GetFileNameWithoutExtension(csvPath);
            string dir = Path.GetDirectoryName(csvPath);
            chunkListPath = dir + "\\" + fname + infolistExt;

            int chunk = 1;

            // Enusure subdir Chunks exists
            string chunkSubDir = dir+"\\Chunks";
            if (Directory.Exists(chunkSubDir) == false)
                Directory.CreateDirectory(chunkSubDir);

            string chunkPath = GetChunkPath(dir, fname, chunk);
            StreamWriter sw = null;
            int chunksize = 0;
            int row = 0;
            CsvChunk chunkInfo = null;

            using (StreamReader sr = new StreamReader(csvPath, Encoding.Default))
            {
                string line = string.Empty;
                do
                {
                    header = sr.ReadLine();
                    if (header == null)
                        throw new ArgumentException("Unexpected End Of File detected.");
                } while (header[0] == properties.CommentMarker);

                try
                {
                    // Remember header and create 1st chunk
                    header = header.ToUpper();
                    sw = CreateChunkWriter(chunkPath);
                    chunkInfo = new CsvChunk(chunk, row + 1);

                    while ((line = sr.ReadLine()) != null)
                    {
                        // Skip any comment
                        if (line[0] == properties.CommentMarker)
                            continue;
                        ++row;
                        sw.WriteLine(line);
                        chunksize += line.Length;

                        // Time for another chunk ?
                        if (chunksize > properties.ChunkSize)
                        {
                            sw.Close();
                            chunkInfo.RowEnd = row;
                            ChunkList.Add(chunkInfo);

                            ++chunk;
                            chunksize = 0;
                            chunkPath = GetChunkPath(dir, fname, chunk);
                            chunkInfo = new CsvChunk(chunk, row + 1);
                            sw = CreateChunkWriter(chunkPath);
                        }
                    }
                }
                finally
                {
                    if (sw != null)
                        sw.Close();
                }

                if (chunkInfo != null)
                {
                    chunkInfo.RowEnd = row;
                    if (chunkInfo.RowTotal > 0)
                        ChunkList.Add(chunkInfo);

                    // Persist chunk info list
                    ChunkList.Serialize(chunkListPath);
                }

            }

            return chunkListPath;
        }

        // Create chunk filepath
        private string GetChunkPath(string dir, string fname, int chunk)
        {
            return dir + "\\Chunks\\" + fname + ".ck" + String.Format("{0:000000}", chunk);
        }

        // Create chunk filepath
        public string GetChunkPath(string csvPath, int chunk)
        {
            string fname = Path.GetFileNameWithoutExtension(csvPath);
            string dir = Path.GetDirectoryName(csvPath);
            return dir + "\\Chunks\\" + fname + ".ck" + String.Format("{0:000000}", chunk);
        }

        /// <summary>Create a writer for a specific chunk. Insert header at start </summary>
        /// <param name="path">path for chunk file</param>
        /// <returns>an opened writer to use for chunk rows</returns>
        public StreamWriter CreateChunkWriter(string path)
        {
            StreamWriter sw = new StreamWriter(path, false, Encoding.Default, 32768);
            sw.WriteLine(header);

            return sw;
        }
        #endregion

        #region Chunk candidate detection

        public bool IsChunckCandidate(string csvPath)
        {
            bool isCandidate = false;
            if (!File.Exists(csvPath))
                throw new FileNotFoundException("The csv file could not be found", csvPath);

            FileInfo f = new FileInfo(csvPath);
            if (f.Length > properties.HugeSize)
                isCandidate = true;

            return isCandidate;
        }

        public string GetChunkInfoListPath(string csvPath)
        {
            if (!File.Exists(csvPath))
                throw new FileNotFoundException("The csv file could not be found", csvPath);
            string fname = Path.GetFileNameWithoutExtension(csvPath);
            string dir = Path.GetDirectoryName(csvPath);

            return dir + "\\" + fname + infolistExt;

        }
        public static string GetChunkInfoErrorPath(string csvPath)
        {
            if (!File.Exists(csvPath))
                throw new FileNotFoundException("The csv file could not be found", csvPath);
            string fname = Path.GetFileNameWithoutExtension(csvPath);
            string dir = Path.GetDirectoryName(csvPath);

            return dir + "\\" + fname + "_Err.xml";

        }

        public bool GetChunkInfoList(string csvPath)
        {
            bool loaded = false;
            string chunkListpath = GetChunkInfoListPath(csvPath);
            if (File.Exists(chunkListpath))
            {
                ChunkList = CsvChunkList.Load(chunkListpath);
                loaded = true;
            }
            return loaded;
        }

        public bool IsChunkInfoListFile(string path)
        {
            bool isChunkList = false;
            if (!File.Exists(path))
                throw new FileNotFoundException("The specified file could not be found", csvPath);

            string ext = Path.GetExtension(path);
            if (ext == infolistExt)
            {
                try
                {
                    ChunkList = CsvChunkList.Load(path);
                    isChunkList = true;
                }
                catch { }
            }
            return isChunkList;
        }

        #endregion

    }


    #region Chunk info Class

    /// <summary>
    /// Class holding information about a file chunk
    /// </summary>
    public class CsvChunk
    {
        /// <summary>Chunk ID (index)</summary>
        public int ChunkId { get; set; }

        /// <summary>Start row number in this chunk</summary>
        public int RowStart { get; set; }

        /// <summary>End row number in this chunk</summary>
        public int RowEnd { get; set; }

        /// <summary>Total rows in chunk</summary>
        public int RowTotal { get { return RowEnd - RowStart + 1; } }

        /// <summary>Construct a chunk bloc info </summary>
        /// <param name="id">chunk ID (as indexer)</param>
        /// <param name="row">Start row number in this chunk</param>
        /// <param name="chunkPath">CSV chunk file path</param>
        public CsvChunk(int id, int row)
        {
            ChunkId = id;
            RowStart = row;
            RowEnd = row; // has to be updated
        }

        /// <summary> Empty C'tor </summary>
        public CsvChunk()
        {
        }
    }
    #endregion

    #region Chunk info List class

    /// <summary>/ Class for hilding a list of chunk info.</summary>
    public class CsvChunkList : List<CsvChunk>
    {
        public CsvChunkList()
        {
        }

        public void Serialize(string path)
        {
            using (FileStream fs = new FileStream(path, FileMode.Create))
            {
                XmlSerializer ser = new XmlSerializer(typeof(CsvChunkList));
                ser.Serialize(fs, this);
                fs.Flush();
                fs.Close();
            }
        }
        public static CsvChunkList Load(string path)
        {
            if (!File.Exists(path))
                throw new FileNotFoundException("The inventory file could not be found", path);

            CsvChunkList result;

            using (FileStream fs = new FileStream(path, FileMode.Open))
            {
                XmlSerializer ser = new XmlSerializer(typeof(CsvChunkList));
                result = (CsvChunkList)ser.Deserialize(fs);
            }
            return result;
        }

    }
    #endregion

}
